Python для исследователей

Татьяна Рогович, НИУ ВШЭ

Интерактивные визуализации в Plotly

In [1]:
!pip install plotly
Requirement already satisfied: plotly in /Users/anastasiaparsina/opt/anaconda3/lib/python3.9/site-packages (5.6.0)
Requirement already satisfied: six in /Users/anastasiaparsina/opt/anaconda3/lib/python3.9/site-packages (from plotly) (1.16.0)
Requirement already satisfied: tenacity>=6.2.0 in /Users/anastasiaparsina/opt/anaconda3/lib/python3.9/site-packages (from plotly) (8.0.1)
In [2]:
import plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd

Мы будем использовать оффлайн версию. Если вы хотите хранить графики в облаке, используйте

https://plot.ly/python/getting-started/#chart-studio-support

Вам понадобится регистрация и создание своего API ключа.

Простые графики в Ploltly

Синтаксис plotly несколько отличается от того, что мы уже видели в matplotlib.

Здесь мы передаем данные для графиков функция из библиотеки plotly.graph_objects, которую мы импортировали как go, и потом эти графики передаем функции go.Figure, которая собственно рендерит наш график.

In [3]:
our_data = [2, 3, 1] # задаем данные 

our_bar = go.Bar(y = our_data) # передаем данные объекту Bar, говорим, что наши данные, это величина категории по шкале y

fig = go.Figure(our_bar) # передаем наш бар объекту Figure, который уже рисует график (ура, что-то знакомое!)

fig.show() # выводим график

А теперь давайте представим, что наши данные разбиты по какой-то категориальной переменной.

In [4]:
trace0 = go.Bar(y = [2, 3, 1])
trace1 = go.Bar(y = [4, 7, 3])

our_data = [trace0, trace1] # когда объектов больше одного - передаем их списком

fig = go.Figure(our_data) 
fig.show()

Теперь попробуем построить что-то с координатами x и y. Такой график уже будет Scatter - у каждого нашего наблюдения есть координаты x и y.

In [5]:
trace0 = go.Scatter(  
    x=[1, 2, 3, 4],
    y=[10, 15, 13, 17]
)
trace1 = go.Scatter(
    x=[1, 2, 3, 4],
    y=[16, 5, 11, 9]
)
our_data = [trace0, trace1] 

fig = go.Figure(our_data)

fig.update_layout(width=800, height=800)

fig.show()

#fig.write_image("fig1.png")

Давайте теперь попробуем построить пару уже знакомых нам графиков для обитателей леса.

In [6]:
forest = pd.read_csv('https://raw.githubusercontent.com/aaparshina/DDC_22-23_basics_python/main/data/populations.txt', sep = '\t')
In [7]:
forest.head()
Out[7]:
year hare lynx carrot
0 1900 30000.0 4000.0 48300
1 1901 47200.0 6100.0 48200
2 1902 70200.0 9800.0 41500
3 1903 77400.0 35200.0 38200
4 1904 36300.0 59400.0 40600

Упражнения

  1. Постройте линеный тренд, который сравнивает популяции зайцев и морковки за все годы.
  2. Постройте столбчатый график, который сравнивает общую популяцию (зайцы, рыси и морковки) с популяцией рысей по годам.
In [8]:
# упражнение 1

trace_hare = go.Scatter(  
    x=forest.year,
    y=forest.hare
)
trace_carrot = go.Scatter(
    x=forest.year,
    y=forest.carrot
)
our_data = [trace_hare, trace_carrot] 

fig = go.Figure(our_data)

fig.show()
In [9]:
# упражнение 

trace_all = go.Bar(  
    x=forest.year,
    y=forest.hare + forest.lynx + forest.carrot
)
trace_lynx = go.Bar(
    x=forest.year,
    y=forest.lynx
)
our_data = [trace_all, trace_lynx] 

fig = go.Figure(our_data)

fig.show()

А теперь давайте построим эти два графика рядом.

Обратите внимание, plotly считает с 1, а не с 0, как мы привыкли.

In [10]:
fig = make_subplots(rows=2, cols=1)

fig.add_trace(trace_carrot, row=1, col=1)
fig.add_trace(trace_hare, row=1, col=1)
fig.add_trace(trace_all, row=2, col=1)
fig.add_trace(trace_lynx, row=2, col=1)

В plotly за данные внутри оси координаты и всю "красоту" (подписи, шкалы, фон, сетка и т.д.) отвечают два разных объекта - data и layout.

'fig = go.Figure(data = our_data)'

Здесь объект data принимает данные, из которых figure построит нам график. Как мы увидим ниже, аттрибуты данных тоже настраиваются объекте данных (например, цвет или размер точек).

За внешний вид этого графика отвечает layout - там довольно много параметров, которые можно настроить, которые задаются через словари, где ключ - параметр, а значение - то, как мы хотим его изменить (текст, числовое значение и т.д.).

https://plot.ly/python/reference/ - здесь можно посмотреть, какие типы графиков вообще есть и какие параметры можно настраивать в каждом из них.

В объект layout мы передаем словарь, где ключ - ключевое слово, а значение - то, что мы ему присваиваем. Обратите внимание, в синтаксе ниже показаны три варианта, как это можно записать. Все они эквивалентны.

In [11]:
trace0 = go.Scatter(
    x=[1, 2, 3, 4],
    y=[10, 15, 13, 17]
)

our_data = [trace0]
our_layout = dict(title = 'A simple line')
# our_layout = {'title' : 'A simple line'}
# our_layout = go.Layout(title = 'A simple line')

# после того, как создали отдельно объекты и для data, и для layout, передаем их функции go.Figure()
fig = go.Figure(data=our_data, layout=our_layout)

fig.show()

Как уже говорилось, все, что внутри осей координат и касается данных - настраивается внутри объекта, относящимся к данным. Так в объекте go.Scatter (который по сути создает словарь, вообще почти все в plotly построено на синтаксисе словарей) мы можем прописать тип, цвет и размер маркеров, всплывающий текст и т.д.). В layout подписываем шкалы x и y - обратите внимание, что внутри словаря некторые параметры в свою очередь тоже словари :)

In [12]:
trace0 = go.Scatter(
    x=[1, 2, 3, 4],
    y=[10, 15, 13, 17],
    marker={'color': 'red', 'symbol': 'x-open', 'size': 10}, # аттрибуты маркера - цвет, код символа, размер
    mode = 'lines+markers', # атрибуты графика. Здесь можно задать просто линию или маркеры, например
    text = ['one', 'two', 'three', 'four'], # подписи к точкам
    name = 'Red Trace' # имя в легенде
)

our_data = [trace0]
our_layout = go.Layout(
    title="First Plot", 
    xaxis={'title':'x axis'}, # заголовки шкал
    yaxis={'title':'y axis'})

# после того, как создали отдельно объекты и для data, и для layout, передаем их функции go.Figure()
fig = go.Figure(data=our_data, layout=our_layout)

fig.show()

Давайте посмотрим, как наши объекты выглядят внутри

In [13]:
# словари словарей!
our_data
Out[13]:
[Scatter({
     'marker': {'color': 'red', 'size': 10, 'symbol': 'x-open'},
     'mode': 'lines+markers',
     'name': 'Red Trace',
     'text': [one, two, three, four],
     'x': [1, 2, 3, 4],
     'y': [10, 15, 13, 17]
 })]
In [14]:
# при желании мы даже можем обратиться к объектам внутри по индексу
our_data[0]['marker']['color']
Out[14]:
'red'
In [15]:
our_layout
Out[15]:
Layout({
    'title': {'text': 'First Plot'}, 'xaxis': {'title': {'text': 'x axis'}}, 'yaxis': {'title': {'text': 'y axis'}}
})

Упражнение

  1. Постройте на одном графике тренды для всех, кто живет в лесу (зайцы, рыси, морковки). Подпишите шкалы, поменяйте цвет всех линий, задайте название графика для легенды.
In [16]:
trace_hare = go.Scatter(  
    x=forest.year,
    y=forest.hare,
    marker={'color': 'grey'},
    name = 'Hares'
)

trace_carrot = go.Scatter(
    x=forest.year,
    y=forest.carrot,
    marker={'color': 'orange'},
    name = 'Carrots'
)


trace_lynx = go.Scatter(
    x=forest.year,
    y=forest.lynx,
    marker={'color': 'teal'},
    name = 'Lynxes'
)
our_data = [trace_hare, trace_carrot, trace_lynx] 

our_layout = go.Layout(
    title="Who is living in the forest?", 
    xaxis={'title':'years'},
    yaxis={'title':'population'})

fig = go.Figure(data = our_data, layout = our_layout)

fig.show()

Упражнение

  1. Вернемся к еще одному знакомому набору данных: постройте график рассеяния для данных по преступности в США, где по шкале x будет количество убийств (murder), по y - ограбления (burglary). За размер будет отвечать количество людей в штате (возможно, нуждается в масшатабировании), а за цвет - количество угнанных автомобилей. При наведении курсора на точку должно выводиться названия штата (обратите внимание на атрибут текст в примерах выше).

Цвет, размер, прозрачность и цветовая схема указываются в словаре аттрибутов маркера (size, color, opacity, colorscale, showscale).

In [17]:
crimes = pd.read_csv('https://raw.githubusercontent.com/aaparshina/DDC_22-23_basics_python/main/data/crimeRatesByState2005.tsv', sep = '\t')
In [18]:
crimes.head()
Out[18]:
state murder Forcible_rate Robbery aggravated_assult burglary larceny_theft motor_vehicle_theft population
0 Alabama 8.2 34.3 141.4 247.8 953.8 2650.0 288.3 4627851
1 Alaska 4.8 81.1 80.9 465.1 622.5 2599.1 391.0 686293
2 Arizona 7.5 33.8 144.4 327.4 948.4 2965.2 924.4 6500180
3 Arkansas 6.7 42.9 91.1 386.8 1084.6 2711.2 262.1 2855390
4 California 6.9 26.0 176.1 317.3 693.3 1916.5 712.8 36756666
In [19]:
trace0 = go.Scatter(
    x = crimes['murder'],
    y = crimes['burglary'],
    mode = 'markers',
    marker = dict(size = crimes['population']/500000,
                color = crimes['motor_vehicle_theft'],
                opacity = 0.7,
                colorscale ='Electric',
                showscale =True),
    text = crimes['state'],
    hovertemplate =
    '<b>%{text}</b>' +
    '<br><i>Murders per capita</i>: %{x}' +
    '<br><i>Burglary per capita</i>: %{y}' +
    '<br><i>Motor Vehicle Theft per capita</i>: %{marker.color}' +
    '<br><i>Population</i>: %{marker.size}'
    )

layout= go.Layout(
    title= 'Crime in the USA',
    
    xaxis= dict(
        title= 'Murder rate (number per 100,000 population)',
        
        
        gridwidth= 2,
    ),
    yaxis=dict(
        title= 'Burglary rate (number per 100,000 population)',
        gridwidth= 2,
    ),
)

fig = go.Figure(data = [trace0], layout = layout)
fig

Упражнение

Сделай график рассеяния для данных gapminder.

  1. Преобразуйте ВВП с помощью логарифма.
  2. Отфильтруйте данные только для одного года (например, 1972)
  3. ВВП по шкале X, продолжительность жизни по Y.
  4. За цвет маркера отвечают континенты (не забудьте перевести переменную в категориальную).
  5. За размер - население.
In [20]:
gapminder = pd.read_csv('https://raw.githubusercontent.com/aaparshina/DDC_22-23_basics_python/main/data/gapminderData.csv')
gapminder.head()
Out[20]:
country year pop continent lifeExp gdpPercap
0 Afghanistan 1952 8425333.0 Asia 28.801 779.445314
1 Afghanistan 1957 9240934.0 Asia 30.332 820.853030
2 Afghanistan 1962 10267083.0 Asia 31.997 853.100710
3 Afghanistan 1967 11537966.0 Asia 34.020 836.197138
4 Afghanistan 1972 13079460.0 Asia 36.088 739.981106
In [21]:
import numpy as np
In [22]:
gapminder['log_gdpPercap'] = np.log(gapminder['gdpPercap'])
gapminder['continent'] = pd.Categorical(gapminder['continent'])
gapminder.head()
Out[22]:
country year pop continent lifeExp gdpPercap log_gdpPercap
0 Afghanistan 1952 8425333.0 Asia 28.801 779.445314 6.658583
1 Afghanistan 1957 9240934.0 Asia 30.332 820.853030 6.710344
2 Afghanistan 1962 10267083.0 Asia 31.997 853.100710 6.748878
3 Afghanistan 1967 11537966.0 Asia 34.020 836.197138 6.728864
4 Afghanistan 1972 13079460.0 Asia 36.088 739.981106 6.606625
In [23]:
gapminder_1972 = gapminder[gapminder['year'] == 1972]
In [24]:
trace0 = go.Scatter(
    x = gapminder_1972['log_gdpPercap'],
    y = gapminder_1972['lifeExp'],
    mode = 'markers',
    marker = dict(size = gapminder_1972['pop']/5000000,
                color = gapminder_1972['continent'].cat.codes,
                opacity = 0.7,
                colorscale ='Viridis',
                showscale =False),
    text = gapminder_1972['country'],
    hovertemplate =
    '<b>%{text}</b>' +
    '<br><i>GDG per Capita</i>: %{x}' +
    '<br><i>Life Expectancy</i>: %{y}' +
    "<extra></extra>",
    )

layout = go.Layout(
    title='Life Expectancy v. Per Capita GDP in 1972',
    hovermode='closest',
    xaxis=dict(
        title='GDP per capita',
        
        gridwidth=2,
    ),
    yaxis=dict(
        title='Life Expectancy (years)',
        
        gridwidth=2,
    ),
)


fig = go.Figure(data = [trace0], layout = layout)
fig

#fig.write_image("fig1.png")

На самом деле ценность данных gapminder, что их здорово использовать для создания анимаций. В традиционном синтаксе Plotly это можно сделать, но сейчас мы воспользуемся библиотекой plotly.express.

https://plot.ly/python/plotly-express/

Это библиотека, которая специально была сделана для "быстрых" визуализаций. Я думаю, вы заметили, что синтаксис plotly достаточно громоздкий по сравнению с matplotlib. Но он и более гибкий. Plotly.express больше похожа на matplotlib, и анимацию мы сделаем именно в ней, потому что здесь это сильно проще.

Ниже ссылку, как делать анимации в традиционном plotly

https://plot.ly/python/v3/gapminder-example/#create-frames

In [25]:
import plotly.express as px

# какая переменная отвечает за анимацию?

px.scatter(gapminder, x="gdpPercap", y="lifeExp", animation_frame="year",
           size="pop", color="continent", hover_name="country",
           log_x=True, size_max=55, range_x=[100,100000], range_y=[25,90])

Также в plotly можно создавать интерактивные тепловые карты. Для этого используем функцию Choropleth.

Параметр location mode принимает значения, которые будут отвечать за географические данные, а locations - уже собственно переменную. Если у вас есть набор данных, где колонка с географическими словарями совпадает с внутренним словарем plotly, то даже почти ничего не нужно делать, все распознается автоматически.

Параметр z - данные, которые наносим на тепловую шкалу.

Почитать больше про тепловые карты: https://plot.ly/python/choropleth-maps/

И про все виды интерактивных карт в plotly: https://plot.ly/python/maps/

In [26]:
trace0 = go.Choropleth(
    locationmode = 'country names',
    locations = gapminder_1972['country'],
    text = gapminder_1972['country'],
    z = gapminder_1972['lifeExp']
)

fig = go.Figure(data = [trace0])
fig

Упражнение

Постройте график для ирисов. Каждый тип ирисов должен быть отдельным графиком, объединенными в один. Длина чашелистика (sepal) - шкала x, длина лепестка (petal) - шкала y, размер маркера - ширина лепестка, цвет - тип ирисов.

In [27]:
iris = pd.read_csv('https://raw.githubusercontent.com/aaparshina/DDC_22-23_basics_python/main/data/iris.csv', header = 0)
In [28]:
iris.head()
Out[28]:
sepal_length sepal_width petal_length petal_width species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa
In [29]:
iris.species.unique()
Out[29]:
array(['setosa', 'versicolor', 'virginica'], dtype=object)
In [30]:
setosa = iris[iris.species == 'setosa']
versicolor = iris[iris.species == 'versicolor']
virginica = iris[iris.species == 'virginica']
In [31]:
trace1 = go.Scatter(
    x = setosa['sepal_length'],
    y = setosa['sepal_width'],
    mode = 'markers',
    marker = dict(size = setosa["petal_length"]*10,
                    color = '#FF0000'),
    name = 'iris setosa'
    )

trace2 = go.Scatter(
    x = versicolor['sepal_length'],
    y = versicolor['sepal_width'],
    mode = 'markers',
    marker = dict(size = versicolor["petal_length"]*10,
                    color = '#009900'),
    name = 'iris versicolor'
    )

trace3 = go.Scatter(
    x = virginica['sepal_length'],
    y = virginica['sepal_width'],
    mode = 'markers',
    marker = dict(size = virginica["petal_length"]*10,
                    color = '#3333FF'),
    name ='iris virginica'
    )

layout= go.Layout(
    title= 'Iris clustering',
    hovermode= 'closest',
    xaxis= dict(
        title= 'Sepal Length (in cm)',
        gridwidth= 2,
    ),
    yaxis=dict(
        title= 'Sepal width (in cm)',
        gridwidth= 2,
    ),
    showlegend= True
)


data = [trace1, trace2, trace3]



fig = go.Figure(data=data, layout=layout)

fig.show()